/*******************************************************************************
 * Copyright (c) 2013 QPark Consulting  S.a r.l.
 * 
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html.
 * 
 * Contributors:
 *     Bernhard Hausen - Initial API and implementation
 *
 ******************************************************************************/
package com.qpark.eip.core.spring.security.https;

import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.annotation.PostConstruct;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.springframework.core.io.Resource;

/**
 * If the SSL/TLS implementation's hostname verification logic (TrustManager)
 * fails, the verify method (HostnameVerifier) returns <code>false</code>.
 * 
 * <pre>
 * http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#TrustManager
 * http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#HostnameVerifier
 * </pre>
 * @author bhausen
 */
public class EipX509TrustManager implements X509TrustManager,
		HostnameVerifier {
	/** The {@link Logger}. */
	private final org.slf4j.Logger logger = org.slf4j.LoggerFactory
			.getLogger(EipX509TrustManager.class);
	/** The key store password. */
	private char[] keystorePassword;

	/** {@link Resource} pointing to the service bus JKS key store. */
	private Resource keystore;

	/**
	 * The default X509TrustManager returned by SunX509. We'll delegate
	 * decisions to it, and fall back to the logic in this class if the default
	 * X509TrustManager doesn't trust it.
	 */
	private X509TrustManager sunJSSEX509TrustManager;

	/** The {@link KeyStore} itself. */
	private KeyStore ks;

	/**
	 * Delegate to the default trust manager.
	 * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[],
	 *      java.lang.String)
	 */
	@Override
	public void checkClientTrusted(final X509Certificate[] chain,
			final String authType) throws CertificateException {
		try {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("checkClientTrusted authType {}", authType);
				for (X509Certificate cert : chain) {
					this.logger.trace("checkClientTrusted X509Certificate {}",
							cert);
				}
			}
			this.sunJSSEX509TrustManager.checkClientTrusted(chain, authType);
			this.logger.debug("checkClientTrusted trusted");
		} catch (CertificateException e) {
			this.logger.error("checkClientTrusted NOT trusted");
			throw e;
		}
	}

	/**
	 * Delegate to the default trust manager.
	 * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[],
	 *      java.lang.String)
	 */
	@Override
	public void checkServerTrusted(final X509Certificate[] chain,
			final String authType) throws CertificateException {
		try {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("checkServerTrusted authType {}", authType);
				for (X509Certificate cert : chain) {
					this.logger.trace("checkServerTrusted X509Certificate {}",
							cert);
				}
			}
			this.sunJSSEX509TrustManager.checkServerTrusted(chain, authType);
			this.logger.debug("checkServerTrusted trusted");
		} catch (CertificateException e) {
			this.logger.error("checkServerTrusted NOT trusted");
			throw e;
		}
	}

	/**
	 * Merely pass this through.
	 * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
	 */
	@Override
	public X509Certificate[] getAcceptedIssuers() {
		return this.sunJSSEX509TrustManager.getAcceptedIssuers();
	}

	/**
	 * Initialize.
	 * @throws Exception
	 */
	@PostConstruct
	public void init() throws Exception {
		// create a "default" JSSE X509TrustManager.
		this.ks = KeyStore.getInstance("JKS");
		if (this.keystore != null) {
			this.ks.load(this.keystore.getInputStream(), this.keystorePassword);
		}

		TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509",
				"SunJSSE");
		tmf.init(this.ks);

		TrustManager tms[] = tmf.getTrustManagers();

		/*
		 * Iterate over the returned trust managers, look for an instance of
		 * X509TrustManager. If found, use that as our "default" trust manager.
		 */
		for (TrustManager tm : tms) {
			if (tm instanceof X509TrustManager) {
				this.sunJSSEX509TrustManager = (X509TrustManager) tm;
				return;
			}
		}

		/*
		 * Find some other way to initialize, or else we have to fail the
		 * constructor.
		 */
		throw new Exception("Couldn't initialize");
	}

	/**
	 * @param keystore the key store to set.
	 */
	public void setKeystore(final Resource keystore) {
		this.keystore = keystore;
	}

	/**
	 * @param keystorePassword the keystorePassword to set.
	 */
	public void setKeystorePassword(final String keystorePassword) {
		if (keystorePassword != null) {
			this.keystorePassword = keystorePassword.toCharArray();
		}
	}

	/**
	 * <pre>
	 * http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#HostnameVerifier
	 * </pre>
	 * 
	 * If the SSL/TLS implementation's standard hostname verification logic
	 * fails, the implementation will call the verify method of the class which
	 * implements this interface and is assigned to this HttpsURLConnection
	 * instance. If the callback class can determine that the hostname is
	 * acceptable given the parameters, it should report that the connection
	 * should be allowed. An unacceptable response will cause the connection to
	 * be terminated.
	 * @see javax.net.ssl.HostnameVerifier#verify(java.lang.String,
	 *      javax.net.ssl.SSLSession)
	 */
	@Override
	public boolean verify(final String hostname, final SSLSession session) {
		try {
			this.logger.debug("verify hostname={}", hostname);
			if (hostname != null
					&& session != null
					&& session.getPeerCertificateChain() != null
					&& session.getPeerCertificateChain().length > 0
					&& session.getPeerCertificateChain()[0] != null
					&& session.getPeerCertificateChain()[0].getPublicKey() != null) {
				Certificate cert = this.ks.getCertificate(hostname);
				if (cert != null && cert.getPublicKey() != null) {
					String ksPublicKey = cert.getPublicKey().toString();
					String serverPublicKey = session.getPeerCertificateChain()[0]
							.getPublicKey().toString();
					if (ksPublicKey.equals(serverPublicKey)) {
						return true;
					} else {
						this.logger.debug("verify not matching public keys!");
						this.logger.debug("verify public key from keystore={}",
								ksPublicKey);
						this.logger.debug("verify public key from server  ={}",
								serverPublicKey);
					}
				} else {
					this.logger.debug(
							"verify no cert({}) with PublicKey found.", cert);
				}
			} else {
				this.logger
						.debug("verify no hostname({}) or session with PeerCertificateChain and PublicKey.",
								hostname);
			}
		} catch (KeyStoreException e) {
			this.logger.debug("verify {}", e.getMessage());
		} catch (SSLPeerUnverifiedException e) {
			this.logger.debug("verify {}", e.getMessage());
		}
		return false;
	}
}
